/*
 * Tranquil Java Integrated Development Environment
 *
 * The GNU General Public License Version 3
 *
 * Copyright (C) 2021 Autumn Lamonte
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Autumn Lamonte [AutumnWalksTheLake@gmail.com] ⚧ Trans Liberation Now
 * @version 1
 */
package tjide.project;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;

import gjexer.TTerminalWindow;
import gjexer.TWindow;
import gjexer.io.ReadTimeoutException;

import tjide.build.ExternalJavaCompiler;
import tjide.build.InternalJavaCompiler;
import tjide.debugger.DebuggerListener;
import tjide.debugger.JavaDebugger;
import tjide.ui.TranquilApplication;

/**
 * JavaTarget is a single Java language source file.
 */
public class JavaTarget extends FileTarget implements RunnableTarget {

    /**
     * Translated strings.
     */
    private static ResourceBundle i18n = ResourceBundle.getBundle(JavaTarget.class.getName());

    // ------------------------------------------------------------------------
    // Variables --------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * The external Java compiler.
     */
    private ExternalJavaCompiler externalJdk;

    /**
     * The internal Java compiler.
     */
    private InternalJavaCompiler internalJdk;

    /**
     * If positive, when the debugger is spawned run to a specific line.
     */
    private int runToLocationLine = -1;

    /**
     * If true, this target is runnable.
     */
    private boolean runnable = false;

    /**
     * Arguments to pass to main() if this target is runnable.
     */
    private List<String> runArguments = new ArrayList<String>();

    /**
     * Arguments to pass to the JVM if this target is runnable.
     */
    private List<String> jvmRunArguments = new ArrayList<String>();

    // ------------------------------------------------------------------------
    // Constructors -----------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Public constructor.
     *
     * @param project the Project associated with this target, or null
     * @param name the source filename, relative to the project's
     * source directory
     */
    public JavaTarget(final Project project, final String name) {
        super(name, makeBuildName(project, name));
    }

    // ------------------------------------------------------------------------
    // RunnableTarget ---------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Determine whether or not this Target is runnable.
     *
     * @return true if this target can be executed via the runTarget() method
     */
    public boolean isRunnable() {
        return runnable;
    }

    /**
     * Set whether or not this Target is runnable.
     *
     * @param runnable if true, this target can be executed via the
     * runTarget() method
     */
    public void setRunnable(final boolean runnable) {
        this.runnable = runnable;
    }

    /**
     * Execute this target.
     *
     * @param application the UI top-level application
     * @param project the project metadata
     * @throws NotRunnableException if this Target is not runnable.
     */
    public void runTarget(final TranquilApplication application,
        final Project project) throws NotRunnableException {

        if (isRunnable()) {
            run(application, project);
        } else {
            throw new NotRunnableException("No main() method in class");
        }
    }

    /**
     * Get the arguments to runTarget().
     *
     * @return the arguments
     */
    public List<String> getRunArguments() {
        return new ArrayList<String>(runArguments);
    }

    /**
     * Set the arguments to runTarget().
     *
     * @param arguments the new arguments
     */
    public void setRunArguments(final List<String> arguments) {
        runArguments = new ArrayList<String>(arguments);
    }

    /**
     * Get the JVM arguments to runTarget().
     *
     * @return the arguments
     */
    public List<String> getJvmRunArguments() {
        return new ArrayList<String>(jvmRunArguments);
    }

    /**
     * Set the JVM arguments to runTarget().
     *
     * @param arguments the new arguments
     */
    public void setJvmRunArguments(final List<String> arguments) {
        jvmRunArguments = new ArrayList<String>(arguments);
    }

    // ------------------------------------------------------------------------
    // JavaTarget -------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Convert a source java filename to a build class name.
     *
     * @param project the project this is associated with
     * @param sourceName source filename
     * @return build filename
     */
    private static String makeBuildName(final Project project,
        final String sourceName) {

        String buildName = sourceName.replace(".java", ".class");
        return buildName;
    }

    /**
     * Compile the target.
     *
     * @param project the project metadata
     */
    public void compile(final Project project) {
        // System.err.println("compile() " + name);

        try {
            String str = project.getOption("compiler.java.useExternal");
            if ((str != null) && (str.equals("true"))) {
                // Use external compiler no matter what.
                externalJdk = new ExternalJavaCompiler(this, compileListeners);
                externalJdk.compile(project);
            } else {
                // Use internal compiler.
                internalJdk = new InternalJavaCompiler(this, compileListeners);
                internalJdk.compile(project);
            }
        }
        catch (Exception e) {
            project.displayException(e);
        }
    }

    /**
     * Run the target.
     *
     * @param application the UI top-level application
     * @param project the project metadata
     */
    private void run(final TranquilApplication application,
        final Project project) {

        String jreName = application.getOption("compiler.java.jreBin");
        if (jreName == null) {
            return;
        }

        // Setup debug address ------------------------------------------------
        String debugAddr;
        debugAddr = application.getOption("compiler.java.jreDebuggerAddress");

        // Start with 'localhost', and then if InetAddress can find the local
        // host name use that.
        String host = "localhost";
        try {
            host = InetAddress.getLocalHost().getHostName();
        } catch (IOException e) {
            // Bubble up to the user.
            project.displayException(e);
        }

        // We will start with port 2405, which is unassigned by IANA and
        // between cvspserver (2401) and venus (2430).  We will also try to
        // bind a server and get a port assigned, if that is successful then
        // we will use that port number.
        int port = 2405;
        try {
            ServerSocket server = new ServerSocket(0);
            server.setReuseAddress(true);
            port = server.getLocalPort();
            server.close();
        } catch (IOException e) {
            // Bubble up to the user.
            project.displayException(e);
        }

        // Replace port in debugAddr
        debugAddr = MessageFormat.format(debugAddr, host,
            Integer.toString(port));

        // Setup class name ---------------------------------------------------
        String className = buildFilename.replace(".class", "");
        if (project.getBuildDir().length() > 0) {
            if (className.startsWith(project.getBuildDir())) {
                className = className.substring(project.getBuildDir().
                    length() + 1);
            }
        }

        // Setup JVM run arguments --------------------------------------------
        String jvmArgs = "";
        for (String arg: jvmRunArguments) {
            jvmArgs += " " + arg;
        }

        // Setup third-party jars ---------------------------------------------
        String projectJars = "";
        // Start with the resources dir itself.
        projectJars += File.pathSeparator + project.getFullResourcesDir();
        // Now add each jar.
        for (String jar: project.getJars()) {
            projectJars += File.pathSeparator + project.getRootDir() +
                File.separator + project.getResourcesDir() + File.separator +
                jar;
        }

        // Setup run arguments ------------------------------------------------
        String runArgs = "";
        for (String arg: runArguments) {
            runArgs += " " + arg;
        }

        // Spawn JVM ----------------------------------------------------------
        String commandLine = MessageFormat.format(jreName,
            debugAddr, jvmArgs, project.getRootDir(), project.getBuildDir(),
            projectJars, className, runArgs);

        /*
        System.err.println("Execute: " + commandLine);
        System.err.println("Execute [split]: " +
            java.util.Arrays.asList(commandLine.split("\\s")));
        */

        // Spin up the output window.  The program will probably start
        // suspended, unless the user altered the jreBin value.
        TTerminalWindow output = new TTerminalWindow(application, 0, 0,
            TWindow.CENTERED | TWindow.RESIZABLE, commandLine.split("\\s+"),
            false) {

            @Override
            public void onClose() {
                super.onClose();
                application.setDebugProgramExited(getExitValue());
            }
        };

        output.setTitle(MessageFormat.format(i18n.getString("outputWindowName"),
                (new File(name)).getName()));
        application.setOutputWindow(output);

        List<DebuggerListener> listeners = new ArrayList<DebuggerListener>();
        listeners.add(application);

        // Wait for some output from the launched JVM, and then create the
        // debugger to attach to it.
        if (output.waitForOutput(5000)) {
            try {
                new JavaDebugger(application, host, port, listeners,
                    project.getBreakpoints(), this, runToLocationLine);
            } catch (Throwable t) {
                if (t instanceof Exception) {
                    project.displayException((Exception) t);
                } else {
                    project.displayException(new RuntimeException(t));
                }
            }
        } else {
            // Emit a message that the JVM did not appear to launch within
            // the timeout period.
            project.displayException(new ReadTimeoutException(
                i18n.getString("jvmTimeout")));
        }
    }

    /**
     * Get this target's Java class name.
     *
     * @param project the Project associated with this target, or null
     * @return the class name
     */
    public String getTargetClassName(final Project project) {
        String name = getBuildFilename().replace(".class", "");
        if (project != null) {
            if (project.getBuildDir().length() > 0) {
                if (name.startsWith(project.getBuildDir())) {
                    name = name.substring(project.getBuildDir().length() + 1);
                }
            }
        }
        return name.replace(File.separator, ".");
    }

    /**
     * When this target is run, go to a specific line location.
     *
     * @param line the line number
     */
    @Override
    public void runToLocation(final int line) {
        runToLocationLine = line;
    }

}
